/*
 * @Author: mrdoob / http://mrdoob.com/; supereggbert / http://www.paulbrunt.co.uk/; julianwa / https://github.com/julianwa
 * @Date:   2018-05-08 19:07:58
 * @Last Modified by:   xiaoyangzhao
 * @Last Modified time: 2018-05-08 19:12:19
 */

export default function Projects(THREE, callback) {
	THREE.RenderableObject = function() {
		this.id = 0

		this.object = null
		this.z = 0
		this.renderOrder = 0
	}

	THREE.RenderableFace = function() {
		this.id = 0

		this.v1 = new THREE.RenderableVertex()
		this.v2 = new THREE.RenderableVertex()
		this.v3 = new THREE.RenderableVertex()

		this.normalModel = new THREE.Vector3()

		this.vertexNormalsModel = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]
		this.vertexNormalsLength = 0

		this.color = new THREE.Color()
		this.material = null
		this.uvs = [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]

		this.z = 0
		this.renderOrder = 0
	}

	//

	THREE.RenderableVertex = function() {
		this.position = new THREE.Vector3()
		this.positionWorld = new THREE.Vector3()
		this.positionScreen = new THREE.Vector4()

		this.visible = true
	}

	THREE.RenderableVertex.prototype.copy = function(vertex) {
		this.positionWorld.copy(vertex.positionWorld)
		this.positionScreen.copy(vertex.positionScreen)
	}

	//

	THREE.RenderableLine = function() {
		this.id = 0

		this.v1 = new THREE.RenderableVertex()
		this.v2 = new THREE.RenderableVertex()

		this.vertexColors = [new THREE.Color(), new THREE.Color()]
		this.material = null

		this.z = 0
		this.renderOrder = 0
	}

	//

	THREE.RenderableSprite = function() {
		this.id = 0

		this.object = null

		this.x = 0
		this.y = 0
		this.z = 0

		this.rotation = 0
		this.scale = new THREE.Vector2()

		this.material = null
		this.renderOrder = 0
	}

	//

	THREE.Projector = function() {
		var _object; var _objectCount; var _objectPool = []
			var _objectPoolLength = 0
			var _vertex; var _vertexCount; var _vertexPool = []
			var _vertexPoolLength = 0
			var _face; var _faceCount; var _facePool = []
			var _facePoolLength = 0
			var _line; var _lineCount; var _linePool = []
			var _linePoolLength = 0
			var _sprite; var _spriteCount; var _spritePool = []
			var _spritePoolLength = 0

			var _renderData = {
				objects: [],
				lights: [],
				elements: []
			}

			var _vector3 = new THREE.Vector3()
			var _vector4 = new THREE.Vector4()

			var _clipBox = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1))
			var _boundingBox = new THREE.Box3()
			var _points3 = new Array(3)

			var _viewMatrix = new THREE.Matrix4()
			var _viewProjectionMatrix = new THREE.Matrix4()

			var _modelMatrix
			var _modelViewProjectionMatrix = new THREE.Matrix4()

			var _normalMatrix = new THREE.Matrix3()

			var _frustum = new THREE.Frustum()

			var _clippedVertex1PositionScreen = new THREE.Vector4()
			var _clippedVertex2PositionScreen = new THREE.Vector4()

		//

		this.projectVector = function(vector, camera) {
			console.warn('THREE.Projector: .projectVector() is now vector.project().')
			vector.project(camera)
		}

		this.unprojectVector = function(vector, camera) {
			console.warn('THREE.Projector: .unprojectVector() is now vector.unproject().')
			vector.unproject(camera)
		}

		this.pickingRay = function() {
			console.error('THREE.Projector: .pickingRay() is now raycaster.setFromCamera().')
		}

		//

		var RenderList = function() {
			var normals = []
			var colors = []
			var uvs = []

			var object = null
			var material = null

			var normalMatrix = new THREE.Matrix3()

			function setObject(value) {
				object = value
				material = object.material

				normalMatrix.getNormalMatrix(object.matrixWorld)

				normals.length = 0
				colors.length = 0
				uvs.length = 0
			}

			function projectVertex(vertex) {
				var position = vertex.position
				var positionWorld = vertex.positionWorld
				var positionScreen = vertex.positionScreen

				positionWorld.copy(position).applyMatrix4(_modelMatrix)
				positionScreen.copy(positionWorld).applyMatrix4(_viewProjectionMatrix)

				var invW = 1 / positionScreen.w

				positionScreen.x *= invW
				positionScreen.y *= invW
				positionScreen.z *= invW

				vertex.visible = positionScreen.x >= -1 && positionScreen.x <= 1 &&
					positionScreen.y >= -1 && positionScreen.y <= 1 &&
					positionScreen.z >= -1 && positionScreen.z <= 1
			}

			function pushVertex(x, y, z) {
				_vertex = getNextVertexInPool()
				_vertex.position.set(x, y, z)

				projectVertex(_vertex)
			}

			function pushNormal(x, y, z) {
				normals.push(x, y, z)
			}

			function pushColor(r, g, b) {
				colors.push(r, g, b)
			}

			function pushUv(x, y) {
				uvs.push(x, y)
			}

			function checkTriangleVisibility(v1, v2, v3) {
				if (v1.visible === true || v2.visible === true || v3.visible === true) return true

				_points3[0] = v1.positionScreen
				_points3[1] = v2.positionScreen
				_points3[2] = v3.positionScreen

				return _clipBox.intersectsBox(_boundingBox.setFromPoints(_points3))
			}

			function checkBackfaceCulling(v1, v2, v3) {
				return ((v3.positionScreen.x - v1.positionScreen.x) *
					(v2.positionScreen.y - v1.positionScreen.y) -
					(v3.positionScreen.y - v1.positionScreen.y) *
					(v2.positionScreen.x - v1.positionScreen.x)) < 0
			}

			function pushLine(a, b) {
				var v1 = _vertexPool[a]
				var v2 = _vertexPool[b]

				// Clip

				v1.positionScreen.copy(v1.position).applyMatrix4(_modelViewProjectionMatrix)
				v2.positionScreen.copy(v2.position).applyMatrix4(_modelViewProjectionMatrix)

				if (clipLine(v1.positionScreen, v2.positionScreen) === true) {
					// Perform the perspective divide
					v1.positionScreen.multiplyScalar(1 / v1.positionScreen.w)
					v2.positionScreen.multiplyScalar(1 / v2.positionScreen.w)

					_line = getNextLineInPool()
					_line.id = object.id
					_line.v1.copy(v1)
					_line.v2.copy(v2)
					_line.z = Math.max(v1.positionScreen.z, v2.positionScreen.z)
					_line.renderOrder = object.renderOrder

					_line.material = object.material

					if (object.material.vertexColors === THREE.VertexColors) {
						_line.vertexColors[0].fromArray(colors, a * 3)
						_line.vertexColors[1].fromArray(colors, b * 3)
					}

					_renderData.elements.push(_line)
				}
			}

			function pushTriangle(a, b, c) {
				var v1 = _vertexPool[a]
				var v2 = _vertexPool[b]
				var v3 = _vertexPool[c]

				if (checkTriangleVisibility(v1, v2, v3) === false) return

				if (material.side === THREE.DoubleSide || checkBackfaceCulling(v1, v2, v3) === true) {
					_face = getNextFaceInPool()

					_face.id = object.id
					_face.v1.copy(v1)
					_face.v2.copy(v2)
					_face.v3.copy(v3)
					_face.z = (v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z) / 3
					_face.renderOrder = object.renderOrder

					// use first vertex normal as face normal

					_face.normalModel.fromArray(normals, a * 3)
					_face.normalModel.applyMatrix3(normalMatrix).normalize()

					for (var i = 0; i < 3; i++) {
						var normal = _face.vertexNormalsModel[i]
						normal.fromArray(normals, arguments[i] * 3)
						normal.applyMatrix3(normalMatrix).normalize()

						var uv = _face.uvs[i]
						uv.fromArray(uvs, arguments[i] * 2)
					}

					_face.vertexNormalsLength = 3

					_face.material = object.material

					_renderData.elements.push(_face)
				}
			}

			return {
				setObject: setObject,
				projectVertex: projectVertex,
				checkTriangleVisibility: checkTriangleVisibility,
				checkBackfaceCulling: checkBackfaceCulling,
				pushVertex: pushVertex,
				pushNormal: pushNormal,
				pushColor: pushColor,
				pushUv: pushUv,
				pushLine: pushLine,
				pushTriangle: pushTriangle
			}
		}

		var renderList = new RenderList()

		function projectObject(object) {
			if (object.visible === false) return

			if (object instanceof THREE.Light) {
				_renderData.lights.push(object)
			} else if (object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Points) {
				if (object.material.visible === false) return
				if (object.frustumCulled === true && _frustum.intersectsObject(object) === false) return

				addObject(object)
			} else if (object instanceof THREE.Sprite) {
				if (object.material.visible === false) return
				if (object.frustumCulled === true && _frustum.intersectsSprite(object) === false) return

				addObject(object)
			}

			var children = object.children

			for (var i = 0, l = children.length; i < l; i++) {
				projectObject(children[i])
			}
		}

		function addObject(object) {
			_object = getNextObjectInPool()
			_object.id = object.id
			_object.object = object

			_vector3.setFromMatrixPosition(object.matrixWorld)
			_vector3.applyMatrix4(_viewProjectionMatrix)
			_object.z = _vector3.z
			_object.renderOrder = object.renderOrder

			_renderData.objects.push(_object)
		}

		this.projectScene = function(scene, camera, sortObjects, sortElements) {
			_faceCount = 0
			_lineCount = 0
			_spriteCount = 0

			_renderData.elements.length = 0

			if (scene.autoUpdate === true) scene.updateMatrixWorld()
			if (camera.parent === null) camera.updateMatrixWorld()

			_viewMatrix.copy(camera.matrixWorldInverse)
			_viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix)

			_frustum.setFromMatrix(_viewProjectionMatrix)

			//

			_objectCount = 0

			_renderData.objects.length = 0
			_renderData.lights.length = 0

			projectObject(scene)

			if (sortObjects === true) {
				_renderData.objects.sort(painterSort)
			}

			//

			var objects = _renderData.objects

			for (var o = 0, ol = objects.length; o < ol; o++) {
				var object = objects[o].object
				var geometry = object.geometry

				renderList.setObject(object)

				_modelMatrix = object.matrixWorld

				_vertexCount = 0

				if (object instanceof THREE.Mesh) {
					if (geometry instanceof THREE.BufferGeometry) {
						var attributes = geometry.attributes
						var groups = geometry.groups

						if (attributes.position === undefined) continue

						var positions = attributes.position.array

						for (var i = 0, l = positions.length; i < l; i += 3) {
							renderList.pushVertex(positions[i], positions[i + 1], positions[i + 2])
						}

						if (attributes.normal !== undefined) {
							var normals = attributes.normal.array

							for (var i = 0, l = normals.length; i < l; i += 3) {
								renderList.pushNormal(normals[i], normals[i + 1], normals[i + 2])
							}
						}

						if (attributes.uv !== undefined) {
							var uvs = attributes.uv.array

							for (var i = 0, l = uvs.length; i < l; i += 2) {
								renderList.pushUv(uvs[i], uvs[i + 1])
							}
						}

						if (geometry.index !== null) {
							var indices = geometry.index.array

							if (groups.length > 0) {
								for (var g = 0; g < groups.length; g++) {
									var group = groups[g]

									for (var i = group.start, l = group.start + group.count; i < l; i += 3) {
										renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2])
									}
								}
							} else {
								for (var i = 0, l = indices.length; i < l; i += 3) {
									renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2])
								}
							}
						} else {
							for (var i = 0, l = positions.length / 3; i < l; i += 3) {
								renderList.pushTriangle(i, i + 1, i + 2)
							}
						}
					} else if (geometry instanceof THREE.Geometry) {
						var vertices = geometry.vertices
						var faces = geometry.faces
						var faceVertexUvs = geometry.faceVertexUvs[0]

						_normalMatrix.getNormalMatrix(_modelMatrix)

						var material = object.material

						var isMultiMaterial = Array.isArray(material)

						for (var v = 0, vl = vertices.length; v < vl; v++) {
							var vertex = vertices[v]

							_vector3.copy(vertex)

							if (material.morphTargets === true) {
								var morphTargets = geometry.morphTargets
								var morphInfluences = object.morphTargetInfluences

								for (var t = 0, tl = morphTargets.length; t < tl; t++) {
									var influence = morphInfluences[t]

									if (influence === 0) continue

									var target = morphTargets[t]
									var targetVertex = target.vertices[v]

									_vector3.x += (targetVertex.x - vertex.x) * influence
									_vector3.y += (targetVertex.y - vertex.y) * influence
									_vector3.z += (targetVertex.z - vertex.z) * influence
								}
							}

							renderList.pushVertex(_vector3.x, _vector3.y, _vector3.z)
						}

						for (var f = 0, fl = faces.length; f < fl; f++) {
							var face = faces[f]

							material = isMultiMaterial === true
								? object.material[face.materialIndex]
								: object.material

							if (material === undefined) continue

							var side = material.side

							var v1 = _vertexPool[face.a]
							var v2 = _vertexPool[face.b]
							var v3 = _vertexPool[face.c]

							if (renderList.checkTriangleVisibility(v1, v2, v3) === false) continue

							var visible = renderList.checkBackfaceCulling(v1, v2, v3)

							if (side !== THREE.DoubleSide) {
								if (side === THREE.FrontSide && visible === false) continue
								if (side === THREE.BackSide && visible === true) continue
							}

							_face = getNextFaceInPool()

							_face.id = object.id
							_face.v1.copy(v1)
							_face.v2.copy(v2)
							_face.v3.copy(v3)

							_face.normalModel.copy(face.normal)

							if (visible === false && (side === THREE.BackSide || side === THREE.DoubleSide)) {
								_face.normalModel.negate()
							}

							_face.normalModel.applyMatrix3(_normalMatrix).normalize()

							var faceVertexNormals = face.vertexNormals

							for (var n = 0, nl = Math.min(faceVertexNormals.length, 3); n < nl; n++) {
								var normalModel = _face.vertexNormalsModel[n]
								normalModel.copy(faceVertexNormals[n])

								if (visible === false && (side === THREE.BackSide || side === THREE.DoubleSide)) {
									normalModel.negate()
								}

								normalModel.applyMatrix3(_normalMatrix).normalize()
							}

							_face.vertexNormalsLength = faceVertexNormals.length

							var vertexUvs = faceVertexUvs[f]

							if (vertexUvs !== undefined) {
								for (var u = 0; u < 3; u++) {
									_face.uvs[u].copy(vertexUvs[u])
								}
							}

							_face.color = face.color
							_face.material = material

							_face.z = (v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z) / 3
							_face.renderOrder = object.renderOrder

							_renderData.elements.push(_face)
						}
					}
				} else if (object instanceof THREE.Line) {
					_modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix)

					if (geometry instanceof THREE.BufferGeometry) {
						var attributes = geometry.attributes

						if (attributes.position !== undefined) {
							var positions = attributes.position.array

							for (var i = 0, l = positions.length; i < l; i += 3) {
								renderList.pushVertex(positions[i], positions[i + 1], positions[i + 2])
							}

							if (attributes.color !== undefined) {
								var colors = attributes.color.array

								for (var i = 0, l = colors.length; i < l; i += 3) {
									renderList.pushColor(colors[i], colors[i + 1], colors[i + 2])
								}
							}

							if (geometry.index !== null) {
								var indices = geometry.index.array

								for (var i = 0, l = indices.length; i < l; i += 2) {
									renderList.pushLine(indices[i], indices[i + 1])
								}
							} else {
								var step = object instanceof THREE.LineSegments ? 2 : 1

								for (var i = 0, l = (positions.length / 3) - 1; i < l; i += step) {
									renderList.pushLine(i, i + 1)
								}
							}
						}
					} else if (geometry instanceof THREE.Geometry) {
						var vertices = object.geometry.vertices

						if (vertices.length === 0) continue

						v1 = getNextVertexInPool()
						v1.positionScreen.copy(vertices[0]).applyMatrix4(_modelViewProjectionMatrix)

						var step = object instanceof THREE.LineSegments ? 2 : 1

						for (var v = 1, vl = vertices.length; v < vl; v++) {
							v1 = getNextVertexInPool()
							v1.positionScreen.copy(vertices[v]).applyMatrix4(_modelViewProjectionMatrix)

							if ((v + 1) % step > 0) continue

							v2 = _vertexPool[_vertexCount - 2]

							_clippedVertex1PositionScreen.copy(v1.positionScreen)
							_clippedVertex2PositionScreen.copy(v2.positionScreen)

							if (clipLine(_clippedVertex1PositionScreen, _clippedVertex2PositionScreen) === true) {
								// Perform the perspective divide
								_clippedVertex1PositionScreen.multiplyScalar(1 / _clippedVertex1PositionScreen.w)
								_clippedVertex2PositionScreen.multiplyScalar(1 / _clippedVertex2PositionScreen.w)

								_line = getNextLineInPool()

								_line.id = object.id
								_line.v1.positionScreen.copy(_clippedVertex1PositionScreen)
								_line.v2.positionScreen.copy(_clippedVertex2PositionScreen)

								_line.z = Math.max(_clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z)
								_line.renderOrder = object.renderOrder

								_line.material = object.material

								if (object.material.vertexColors === THREE.VertexColors) {
									_line.vertexColors[0].copy(object.geometry.colors[v])
									_line.vertexColors[1].copy(object.geometry.colors[v - 1])
								}

								_renderData.elements.push(_line)
							}
						}
					}
				} else if (object instanceof THREE.Points) {
					_modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix)

					if (geometry instanceof THREE.Geometry) {
						var vertices = object.geometry.vertices

						for (var v = 0, vl = vertices.length; v < vl; v++) {
							var vertex = vertices[v]

							_vector4.set(vertex.x, vertex.y, vertex.z, 1)
							_vector4.applyMatrix4(_modelViewProjectionMatrix)

							pushPoint(_vector4, object, camera)
						}
					} else if (geometry instanceof THREE.BufferGeometry) {
						var attributes = geometry.attributes

						if (attributes.position !== undefined) {
							var positions = attributes.position.array

							for (var i = 0, l = positions.length; i < l; i += 3) {
								_vector4.set(positions[i], positions[i + 1], positions[i + 2], 1)
								_vector4.applyMatrix4(_modelViewProjectionMatrix)

								pushPoint(_vector4, object, camera)
							}
						}
					}
				} else if (object instanceof THREE.Sprite) {
					_vector4.set(_modelMatrix.elements[12], _modelMatrix.elements[13], _modelMatrix.elements[14], 1)
					_vector4.applyMatrix4(_viewProjectionMatrix)

					pushPoint(_vector4, object, camera)
				}
			}

			if (sortElements === true) {
				_renderData.elements.sort(painterSort)
			}

			return _renderData
		}

		function pushPoint(_vector4, object, camera) {
			var invW = 1 / _vector4.w

			_vector4.z *= invW

			if (_vector4.z >= -1 && _vector4.z <= 1) {
				_sprite = getNextSpriteInPool()
				_sprite.id = object.id
				_sprite.x = _vector4.x * invW
				_sprite.y = _vector4.y * invW
				_sprite.z = _vector4.z
				_sprite.renderOrder = object.renderOrder
				_sprite.object = object

				_sprite.rotation = object.rotation

				_sprite.scale.x = object.scale.x * Math.abs(_sprite.x - (_vector4.x + camera.projectionMatrix.elements[0]) / (_vector4.w + camera.projectionMatrix.elements[12]))
				_sprite.scale.y = object.scale.y * Math.abs(_sprite.y - (_vector4.y + camera.projectionMatrix.elements[5]) / (_vector4.w + camera.projectionMatrix.elements[13]))

				_sprite.material = object.material

				_renderData.elements.push(_sprite)
			}
		}

		// Pools

		function getNextObjectInPool() {
			if (_objectCount === _objectPoolLength) {
				var object = new THREE.RenderableObject()
				_objectPool.push(object)
				_objectPoolLength++
				_objectCount++
				return object
			}

			return _objectPool[_objectCount++]
		}

		function getNextVertexInPool() {
			if (_vertexCount === _vertexPoolLength) {
				var vertex = new THREE.RenderableVertex()
				_vertexPool.push(vertex)
				_vertexPoolLength++
				_vertexCount++
				return vertex
			}

			return _vertexPool[_vertexCount++]
		}

		function getNextFaceInPool() {
			if (_faceCount === _facePoolLength) {
				var face = new THREE.RenderableFace()
				_facePool.push(face)
				_facePoolLength++
				_faceCount++
				return face
			}

			return _facePool[_faceCount++]
		}

		function getNextLineInPool() {
			if (_lineCount === _linePoolLength) {
				var line = new THREE.RenderableLine()
				_linePool.push(line)
				_linePoolLength++
				_lineCount++
				return line
			}

			return _linePool[_lineCount++]
		}

		function getNextSpriteInPool() {
			if (_spriteCount === _spritePoolLength) {
				var sprite = new THREE.RenderableSprite()
				_spritePool.push(sprite)
				_spritePoolLength++
				_spriteCount++
				return sprite
			}

			return _spritePool[_spriteCount++]
		}

		//

		function painterSort(a, b) {
			if (a.renderOrder !== b.renderOrder) {
				return a.renderOrder - b.renderOrder
			} else if (a.z !== b.z) {
				return b.z - a.z
			} else if (a.id !== b.id) {
				return a.id - b.id
			} else {
				return 0
			}
		}

		function clipLine(s1, s2) {
			var alpha1 = 0
				var alpha2 = 1

				// Calculate the boundary coordinate of each vertex for the near and far clip planes,
				// Z = -1 and Z = +1, respectively.

				var bc1near = s1.z + s1.w
				var bc2near = s2.z + s2.w
				var bc1far = -s1.z + s1.w
				var bc2far = -s2.z + s2.w

			if (bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0) {
				// Both vertices lie entirely within all clip planes.
				return true
			} else if ((bc1near < 0 && bc2near < 0) || (bc1far < 0 && bc2far < 0)) {
				// Both vertices lie entirely outside one of the clip planes.
				return false
			} else {
				// The line segment spans at least one clip plane.

				if (bc1near < 0) {
					// v1 lies outside the near plane, v2 inside
					alpha1 = Math.max(alpha1, bc1near / (bc1near - bc2near))
				} else if (bc2near < 0) {
					// v2 lies outside the near plane, v1 inside
					alpha2 = Math.min(alpha2, bc1near / (bc1near - bc2near))
				}

				if (bc1far < 0) {
					// v1 lies outside the far plane, v2 inside
					alpha1 = Math.max(alpha1, bc1far / (bc1far - bc2far))
				} else if (bc2far < 0) {
					// v2 lies outside the far plane, v2 inside
					alpha2 = Math.min(alpha2, bc1far / (bc1far - bc2far))
				}

				if (alpha2 < alpha1) {
					// The line segment spans two boundaries, but is outside both of them.
					// (This can't happen when we're only clipping against just near/far but good
					//  to leave the check here for future usage if other clip planes are added.)
					return false
				} else {
					// Update the s1 and s2 vertices to match the clipped line segment.
					s1.lerp(s2, alpha1)
					s2.lerp(s1, 1 - alpha2)

					return true
				}
			}
		}
	}

	callback && callback(THREE)
}
